package com.eugenes.collections;

import com.eugenes.exceptions.MyIndexOutOfBoundsException;
import java.util.Arrays;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;

public final class MyArrayList<E> implements MyList<E>, RandomAccess {

    private final int DEFAULT_CAPACITY = 16;

    @SuppressWarnings("unchecked")
    private final E[] EMPTY_ARRAY = (E[]) new Object[0];

    private E[] data;
    private int size;
    int capacity;

    private int lastMod = Integer.MIN_VALUE;

    public MyArrayList() {
        capacity = DEFAULT_CAPACITY;
        data = getNewArray(capacity);
    }

    public MyArrayList(int capacity) {
        this.capacity = capacity;
        data = getNewArray(this.capacity);
    }

    public MyArrayList(Iterable<? extends E> c) {
        this();
        addAll(c);
    }

    private E[] getNewArray(int size) {
        return Arrays.copyOf(EMPTY_ARRAY, size);
    }

    private void checkIndex(int index, int lower, int upper) {
        if (index >= upper || index < lower) {
            // TODO: change ex-n class to my own ex-n class
            throw new MyIndexOutOfBoundsException("Index out of bounds: " + index + ".");
        }
    }

    private void shiftData(int index, int shift) {
        ensureCapacity(size + shift);
        if (index != size) {
            System.arraycopy(data, index, data, index + shift, size - index);
        }
        size += shift;
        ++lastMod;
    }

    public void ensureCapacity(int minCapacity) {
        long newCapacity = capacity;
        while (minCapacity > newCapacity) {
            newCapacity <<= 1;
            if (newCapacity > Integer.MAX_VALUE) {
                newCapacity = minCapacity;
            }
        }

        if (newCapacity != capacity) {
            capacity = (int)newCapacity;
            data = Arrays.copyOf(data, capacity);
        }
    }

    public void trimToSize() {
        if (capacity != size) {
            capacity = size;
            data = Arrays.copyOf(data, capacity);
        }
    }

    @Override
    public void add(E element) {
        add(size, element);
    }

    @Override
    public void add(int index, E element) {
        checkIndex(index, 0, size + 1);
        shiftData(index, 1);
        data[index] = element;
    }

    private void addAll(Iterable<? extends E> c, int index, int cSize) {
        shiftData(index, cSize);

        for (E element : c) {
            data[index++] = element;
        }
    }

    private static int countIterable(Iterable<?> c) {
        int result = 0;
        for (Iterator<?> it = c.iterator(); it.hasNext(); it.next()) {
            ++result;
        }
        return result;
    }

    @Override
    public void addAll(Iterable<? extends E> c) {
        addAll(c, size, countIterable(c));
    }

    public void addAll(Collection<? extends E> c) {
        addAll(c, size, c.size());
    }

    @Override
    public void addAll(int index, Iterable<? extends E> c) {
        addAll(c, index, countIterable(c));
    }

    public void addAll(int index, Collection<? extends E> c) {
        addAll(c, index, c.size());
    }

    @Override
    public void clear() {
        for (int i=0; i<size; ++i) {
            data[i] = null;
        }
        size = 0;
        ++lastMod;
    }

    @Override
    public E remove(int index) {
        checkIndex(index, 0, size);
        E result = data[index];
        shiftData(index + 1, -1);
        return result;
    }

    @Override
    public void set(int index, E element) {
        checkIndex(index, 0, size);
        data[index] = element;
        ++lastMod;
    }

    @Override
    public E get(int index) {
        checkIndex(index, 0, size);
        return data[index];
    }

    @Override
    public int indexOf(Object obj) {
        for (int i=0; i<size; ++i) {
            if (obj == null ? data[i] == null : obj.equals(data[i])) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return 0 == size();
    }

    @Override
    public Object[] toArray() {
        return Arrays.copyOf(data, size);
    }

    @Override
    public Iterator<E> iterator() {
        return new ArrayIterator();
    }

    private class ArrayIterator implements Iterator<E> {
        private int curIndex = 0;
        private int curMod = lastMod;
        private boolean wasRemoved = true;

        public ArrayIterator() {}

        private void checkModification() {
            if (curMod != lastMod) {
                throw new ConcurrentModificationException("Data has changed since iterator creation.");
            }
        }

        @Override
        public boolean hasNext() {
            return curIndex != size;
        }

        @Override
        public E next() {
            checkModification();
            if (!hasNext()) {
                throw new NoSuchElementException("Iterator is out of bounds.");
            }
            E result = data[curIndex++];
            wasRemoved = false;
            return result;
        }

        @Override
        public void remove() {
            checkModification();
            if (wasRemoved) {
                throw new IllegalStateException("Call next() method before removing element");
            }
            wasRemoved = true;
            MyArrayList.this.remove(curIndex - 1);
            --lastMod; //lifehack
            --curIndex;
        }
    }
}
